Debugging en 4K: Cómo domar el caos de objetos complejos con [DebuggerDisplay]

Introducción

Has llegado a esa línea del código. Pones un breakpoint. Te asomas al debugger, y ves esto:

Name     Value                     Type
order    {MyApp.Domain.Order}      Order

Horrible. Para ver algo útil tienes que expandir el objeto, luego expandir Lines, luego cada línea... una pérdida de tiempo. La buena noticia: Visual Studio te da superpoderes con tres atributos mágicos.

[DebuggerDisplay] — el resumen en una línea

Este atributo controla el texto que aparece en la columna Value del debugger. Puedes usar { } para referenciar campos, propiedades o métodos.

[DebuggerDisplay("{Id}: {Customer} | {Status} | {Total:C}")]
public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public string Status { get; set; }
    public decimal Total { get; set; }
}

Ahora ves:

Name     Value                           Type
order    #1001: Ana | Shipped | $1,245   Order

Mucho mejor.

Buenas prácticas: propiedad privada DebuggerDisplay

No pongas expresiones complejas directo en el atributo. Crea una propiedad privada y usa el sufijo ,nq (no quotes):

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Order
{
    private string DebuggerDisplay =>
        $"#{Id} | {Customer} | {Status} | {Lines?.Count ?? 0} items";
}

El sufijo ,nq evita que el string se muestre entre comillas. La evaluación es perezosa (solo se calcula cuando miras el objeto) y no ensucias la API pública.

[DebuggerTypeProxy] — una vista alternativa sin modificar la clase

Cuando expandes la flecha , puedes controlar qué propiedades se muestran sin tocar la clase real. El proxy puede exponer info calculada que no existe en la clase original:

[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(OrderDebugView))]
public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<OrderLine> Lines { get; set; }
    public decimal Discount { get; set; }
    public string Status { get; set; }

    private string DebuggerDisplay =>
        $"#{Id} | {Customer} | {Status} | {Lines?.Count ?? 0} items";
}

internal class OrderDebugView
{
    private readonly Order _order;
    public OrderDebugView(Order order) => _order = order;

    public string Status => _order.Status;
    public decimal Subtotal => _order.Lines?.Sum(l => l.Price * l.Quantity) ?? 0;
    public decimal Discount => _order.Discount;
    public decimal Total => Subtotal - Discount;
    public int ItemsCount => _order.Lines?.Count ?? 0;
    public TimeSpan Age => DateTime.Now - _order.CreatedAt;
}

Al expandir ves estas propiedades como si fueran parte de Order, pero el código real no cambia.

[DebuggerBrowsable(RootHidden)] — aplanar colecciones

Cuando tienes una lista y la expandes, normalmente ves [0], [1], etc. Con RootHidden ocultas la propiedad colección y muestras sus elementos directamente:

internal class OrderDebugView
{
    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    public OrderLine[] Items => _order.Lines?.ToArray();
}

Así los OrderLine aparecen directamente al expandir Order, sin el nivel intermedio Lines.

Caso real: objetos anidados a 3 niveles

Aquí está el escenario completo con Order → OrderLine → Product → Category:

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Department { get; set; }
    private string DebuggerDisplay => $"[{Department}] {Name}";
}

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Product
{
    public string Sku { get; set; }
    public string Name { get; set; }
    public Category Category { get; set; }
    public decimal UnitPrice { get; set; }
    private string DebuggerDisplay =>
        $"{Name} | SKU: {Sku} | ${UnitPrice}";
}

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class OrderLine
{
    public Product Product { get; set; }
    public int Quantity { get; set; }
    private string DebuggerDisplay =>
        $"{Product?.Name} x{Quantity} = {Quantity * (Product?.UnitPrice ?? 0):C}";
}

[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(OrderDebugView))]
public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public List<OrderLine> Lines { get; set; }
    private string DebuggerDisplay =>
        $"#{Id} | {Customer} | {Lines?.Count ?? 0} lines";
}

internal class OrderDebugView
{
    private readonly Order _order;
    public OrderDebugView(Order order) => _order = order;
    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    public OrderLine[] Items => _order.Lines?.ToArray();
}

Lo que ves en el debugger

Name     Value                                  Type
order    #1001 | Ana | 2 lines                  Order
  ▶ [0]  Laptop x2 = $2,400.00                 OrderLine
           ▶ Product                            Product
             ▶ Category                         Category
               Id: 5
               Name: "Electronics"
               Department: "Tech"
             Sku: "LP-001"
             Name: "Gamer Laptop"
             UnitPrice: 1200
           Quantity: 2
  ▶ [1]  Mouse x1 = $45.00                     OrderLine
           ▶ Product                            Product
             ▶ Category                         Category
               ...

Cada nivel se expande con su propio , y cada uno ya te da la información clave sin tener que picarle.

⚠️ Advertencias importantes

  1. Rendimiento: Las expresiones se evalúan cada vez que el debugger pausa. No pongas operaciones costosas (accesos a BD, llamadas HTTP) dentro de DebuggerDisplay.
  2. Show raw structure: Si activas Tools > Options > Debugging > General > Show raw structure of objects in variables windows, todos estos atributos se ignoran.
  3. Solo C#/CLI: En código nativo C++, solo funciona en C++/CLI.
  4. Herencia: Si pones [DebuggerDisplay] en una clase base, las subclases también lo heredan.

Bonus: DebuggerBrowsableState

Estado Comportamiento
Collapsed Por defecto. La propiedad se muestra colapsada.
RootHidden Oculta la propiedad raíz, muestra sus elementos hijos directamente. Ideal para colecciones.
Never No muestra la propiedad en el debugger.

Conclusión

Con solo tres atributos —DebuggerDisplay, DebuggerTypeProxy y DebuggerBrowsable— transformas la experiencia de debugging. De ver {MyApp.Domain.Order} pasas a tener resúmenes inteligentes, vistas alternativas y colecciones aplanadas.

La regla de oro: si tienes que expandir más de 2 niveles para entender el estado de un objeto, te falta un [DebuggerDisplay] en algún lado.


Este artículo se basa en contenido de Tools and Skills for .NET 8 (Mark J. Price), Systems Programming with C# and .NET (Vasko Sotirovski), y la documentación oficial de Microsoft sobre DebuggerDisplayAttribute.